package com.chenwc.tencentcos;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.Headers;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.http.HttpMethodName;
import com.qcloud.cos.http.HttpProtocol;
import com.qcloud.cos.model.*;
import com.qcloud.cos.region.Region;
import com.qcloud.cos.transfer.*;
import com.qcloud.cos.utils.DateUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 腾讯cos对象存储
 *
 * @author chenwc
 * @date 2024/4/1 21:57
 */
public class CosUtil {

    private static final Logger log = LoggerFactory.getLogger(CosUtil.class);

    private static COSClient cosClient;

    private static String secretId;

    private static String secretKey;

    private static String regionStr;

    /**
     * 文件分块大小,10M
     */
    private static final Integer CHUNK_FILE_SIZE = 10 * 1024 * 1024;

    /**
     * 文件分块阈值,10M
     */
    private static final Integer CHUNK_FILE_SIZE_THRESHOLD = 10 * 1024 * 1024;

    /**
     * 设置accessKey和secretKey
     *
     * @param accessKey     accessKey
     * @param userSecretKey secretKey
     * @param userRegion    地域
     */
    public static void setCosClient(String accessKey, String userSecretKey, String userRegion) {
        secretId = accessKey;
        secretKey = userSecretKey;
        regionStr = userRegion;
        setCosClient();
    }

    /**
     * 获取cos客户端
     */
    public static void setCosClient() {
        if (null == cosClient) {
            // 1 初始化用户身份信息（secretId, secretKey）。
            // SECRETID 和 SECRETKEY 请登录访问管理控制台 https://console.cloud.tencent.com/cam/capi 进行查看和管理
            //String secretId = "";
            //String secretKey = "";
            COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
            // 2 设置 bucket 的地域, COS 地域的简称请参见 https://cloud.tencent.com/document/product/436/6224
            // clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分。
            Region region = new Region(regionStr);
            ClientConfig clientConfig = new ClientConfig(region);
            // 这里建议设置使用 https 协议
            // 从 5.6.54 版本开始，默认使用了 https
            clientConfig.setHttpProtocol(HttpProtocol.https);
            // 3 生成 cos 客户端。
            cosClient = new COSClient(cred, clientConfig);
        }
    }

    /**
     * 关闭cos客户端
     */
    public static void closeCosClient() {
        if (null != cosClient) {
            cosClient.shutdown();
        }
    }

    /**
     * 列出左右存储桶
     *
     * @return 存储桶列表
     */
    public static JSONArray bucketList() {
        setCosClient();
        List<Bucket> buckets = cosClient.listBuckets();
        return JSONArray.parseArray(JSON.toJSONString(buckets, JSONWriter.Feature.WriteMapNullValue));
    }

    /**
     * 判断存储桶是否存在
     *
     * @param bucketName 桶名
     * @return 存在返回true，不存在返回false
     */
    public static boolean isExistBucket(String bucketName) {
        setCosClient();
        List<Bucket> buckets = cosClient.listBuckets();
        for (Bucket bucketElement : buckets) {
            if (bucketName.equals(bucketElement.getName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * 创建存储桶
     *
     * @param bucketName        桶名
     * @param accessControlList 设置 bucket 的权限为 Private(私有读写)、PublicRead（公有读私有写）、PublicReadWrite（公有读写）
     * @return 创建成功返回true，失败返回false
     */
    public static boolean createBucket(String bucketName, CannedAccessControlList accessControlList) {
        setCosClient();
        CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
        // 设置 bucket 的权限为 Private(私有读写)、其他可选有 PublicRead（公有读私有写）、PublicReadWrite（公有读写）
        createBucketRequest.setCannedAcl(accessControlList);
        try {
            Bucket bucketResult = cosClient.createBucket(createBucketRequest);
            log.info("CreateBucketResult: {}", JSON.toJSONString(bucketResult, JSONWriter.Feature.WriteMapNullValue));
        } catch (CosClientException serverException) {
            log.error("Error occurred: ", serverException);
        }
        return isExistBucket(bucketName);
    }

    /**
     * 获取存储桶版本控制配置
     *
     * @param bucketName 桶名
     * @return 存储桶版本控制配置
     */
    public static String getBucketVersion(String bucketName) {
        setCosClient();
        return cosClient.getBucketVersioningConfiguration(bucketName).getStatus();
    }

    /**
     * 设置存储桶版本控制配置
     *
     * @param bucketName                  桶名
     * @param bucketVersioningConfiguration 存储桶版本控制配置
     */
    public static void setBucketVersion(String bucketName, BucketVersioningConfiguration bucketVersioningConfiguration) {
        setCosClient();
        SetBucketVersioningConfigurationRequest setBucketVersioningConfigurationRequest = new SetBucketVersioningConfigurationRequest(bucketName, bucketVersioningConfiguration);
        setBucketVersioningConfigurationRequest.setBucketName(bucketName);
        setBucketVersioningConfigurationRequest.setVersioningConfiguration(bucketVersioningConfiguration);
        cosClient.setBucketVersioningConfiguration(setBucketVersioningConfigurationRequest);
    }

    /**
     * 上传对象
     *
     * @param bucketName 桶名
     * @param key        key
     * @param localFile  待上传的文件
     */
    public static void uploadObject(String bucketName, String key, File localFile) {
        setCosClient();
        // 指定文件上传到 COS 上的路径，即对象键。例如对象键为 folder/picture.jpg，则表示将文件 picture.jpg 上传到 folder 路径下
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, localFile);
        PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
        log.info("putObjectResult: {}", JSON.toJSONString(putObjectResult, JSONWriter.Feature.WriteMapNullValue));
    }

    /**
     * 下载对象
     *
     * @param bucketName     桶名
     * @param key            key
     * @param versionId      指定版本，为空则默认下载最新版本
     * @param outputFilePath 本地文件保存路径
     */
    public static void downloadObject(String bucketName, String key, String versionId, String outputFilePath) {
        setCosClient();
        // 方法1 获取下载输入流
//        GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, key);
//        COSObject cosObject = cosClient.getObject(getObjectRequest);
//        COSObjectInputStream cosObjectInput = cosObject.getObjectContent();
//        // 下载对象的 CRC64
//        String crc64Ecma = cosObject.getObjectMetadata().getCrc64Ecma();
//        // 关闭输入流
//        try {
//            cosObjectInput.close();
//        } catch (IOException e) {
//            log.error("Error occurred: ", e);
//        }

        // 方法2 下载文件到本地的路径，例如 把文件下载到本地的 /path/to/路径下的localFile文件中
        File downFile = new File(outputFilePath);
        GetObjectRequest getObjectRequest;
        if (StringUtils.isNotBlank(versionId)) {
            getObjectRequest = new GetObjectRequest(bucketName, key, versionId);
        } else {
            getObjectRequest = new GetObjectRequest(bucketName, key);
        }
        ObjectMetadata downObjectMeta = cosClient.getObject(getObjectRequest, downFile);
        log.info("downObjectMeta: {}", JSON.toJSONString(downObjectMeta, JSONWriter.Feature.WriteMapNullValue));
    }

    /**
     * 删除对象
     *
     * @param bucketName 桶名
     * @param key        key
     */
    public static void deleteObject(String bucketName, String key) {
        setCosClient();
        cosClient.deleteObject(bucketName, key);
    }

    /**
     * 删除对象
     *
     * @param bucketName 桶名
     * @param key        key
     */
    public static void deleteObject(String bucketName, String key, String versionId) {
        setCosClient();
        cosClient.deleteVersion(bucketName, key, versionId);
    }

    /**
     * 获取对象元数据
     *
     * @param bucketName 桶名
     * @param key        key
     * @param isGetVersion 是否获取版本信息
     * @return 对象元数据
     */
    public static JSONArray getObjectMetadata(String bucketName, String key, boolean isGetVersion) {
        setCosClient();
        ListVersionsRequest listVersionsRequest = new ListVersionsRequest();
        listVersionsRequest.setBucketName(bucketName);
        listVersionsRequest.setPrefix(key);
        VersionListing listing = cosClient.listVersions(listVersionsRequest);
        JSONArray jsonArray = new JSONArray();
        if (!listing.getVersionSummaries().isEmpty()) {
            List<COSVersionSummary> cosVersionSummaries = listing.getVersionSummaries();
            // 过滤出最新版本
            if (!isGetVersion){
                List<COSVersionSummary> cosVersionSummariesNew = cosVersionSummaries.stream().filter(
                        COSVersionSummary::isLatest
                ).collect(Collectors.toList());
                return JSONArray.parseArray(JSON.toJSONString(cosVersionSummariesNew, JSONWriter.Feature.WriteMapNullValue));
            }
            // 获取所有版本
            for (COSVersionSummary cosVersionSummary : cosVersionSummaries) {
                JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(cosVersionSummary, JSONWriter.Feature.WriteMapNullValue));
                jsonArray.add(jsonObject);
            }
        }
        return jsonArray;
    }

    /**
     * 判断对象是否存在
     *
     * @param bucketName 桶名
     * @param key        key
     * @return 存在返回true，不存在返回false
     */
    public static boolean isExistObject(String bucketName, String key) {
        try {
            setCosClient();
            return cosClient.doesObjectExist(bucketName, key);
        } catch (CosClientException e) {
            log.error("Error occurred: ", e);
        }
        return false;
    }

    /**
     * 获取对象的url
     *
     * @param bucketName 桶名
     * @param key        key
     * @return 对象的下载url
     */
    public static String getObjectUrl(String bucketName, String key) {
        setCosClient();
        URL url = cosClient.getObjectUrl(bucketName, key);
        //return url.getProtocol() + "://" + url.getHost() + url.getPath();
        return url.toString();
    }

    /**
     * 生成覆盖返回头部的预签名下载 URL，签名过期时间默认半小时
     *
     * @param bucketName 桶名
     * @param key        key
     * @return url
     */
    public static String generatePresignedUrl(String bucketName, String key) {
        return generatePresignedUrl(bucketName, key, 30, TimeUnit.MINUTES);
    }

    /**
     * 生成覆盖返回头部的预签名下载 URL
     *
     * @param bucketName     桶名
     * @param key            key
     * @param expirationTime 签名过期时间
     * @param timeUnit       签名过期时间的时间单位
     * @return url
     */
    public static String generatePresignedUrl(String bucketName, String key, long expirationTime, TimeUnit timeUnit) {
        setCosClient();
        GeneratePresignedUrlRequest req =
                new GeneratePresignedUrlRequest(bucketName, key, HttpMethodName.GET);

        ObjectMetadata objectMetadata = cosClient.getObjectMetadata(bucketName, key);
        log.info("objectMetadata: {}", JSON.toJSONString(objectMetadata, JSONWriter.Feature.WriteMapNullValue));
        // 设置下载时返回的 http 头
        ResponseHeaderOverrides responseHeaders = new ResponseHeaderOverrides();
        String responseContentType = objectMetadata.getContentType();
        String responseContentLanguage = "zh-CN";
        // 设置返回头部里包含文件名信息
        //String responseContentDispositon = "filename=exampleobject";
        String responseCacheControl = "no-cache";
        String cacheExpireStr = DateUtils.formatRFC822Date(new Date(System.currentTimeMillis() + 24L * 3600L * 1000L));
        responseHeaders.setContentType(responseContentType);
        responseHeaders.setContentLanguage(responseContentLanguage);
        //responseHeaders.setContentDisposition(responseContentDispositon);
        responseHeaders.setCacheControl(responseCacheControl);
        responseHeaders.setExpires(cacheExpireStr);
        req.setResponseHeaders(responseHeaders);
        // 设置签名过期时间(可选)，若未进行设置，则默认使用 ClientConfig 中的签名过期时间(1小时)

        long expirateTime;
        switch (timeUnit) {
            case MILLISECONDS:
                expirateTime = expirationTime;
                break;
            case SECONDS:
                expirateTime = expirationTime * 1000L;
                break;
            case MINUTES:
                expirateTime = expirationTime * 60L * 1000L;
                break;
            case HOURS:
                expirateTime = expirationTime * 3600L * 1000L;
                break;
            case DAYS:
                expirateTime = expirationTime * 24L * 3600L * 1000L;
                break;
            default:
                throw new RuntimeException("时间单位不对，取值范围是：MILLISECONDS、SECONDS、MINUTES、HOURS、DAYS");
        }
        Date expirationDate = new Date(System.currentTimeMillis() + expirateTime);
        req.setExpiration(expirationDate);

        // 填写本次请求的参数
        //req.addRequestParameter("param1", "value1");
        // 填写本次请求的头部
        // host 必填
        req.putCustomRequestHeader(Headers.HOST, cosClient.getClientConfig().getEndpointBuilder().buildGeneralApiEndpoint(bucketName));
        //req.putCustomRequestHeader("header1", "value1");
        URL url = cosClient.generatePresignedUrl(req);
        return url.toString();
    }

    /**
     * 创建 TransferManager 实例，这个实例用来后续调用高级接口
     *
     * @return TransferManager
     */
    public static TransferManager createTransferManager() {
        // 创建一个 COSClient 实例，这是访问 COS 服务的基础实例。
        setCosClient();
        // 自定义线程池大小，建议在客户端与 COS 网络充足（例如使用腾讯云的 CVM，同地域上传 COS）的情况下，设置成16或32即可，可较充分的利用网络资源
        // 对于使用公网传输且网络带宽质量不高的情况，建议减小该值，避免因网速过慢，造成请求超时。
        ExecutorService threadPool = Executors.newFixedThreadPool(32);
        // 传入一个 threadpool, 若不传入线程池，默认 TransferManager 中会生成一个单线程的线程池。
        TransferManager transferManager = new TransferManager(cosClient, threadPool);
        // 设置高级接口的配置项
        // 分块上传阈值和分块大小分别为 5MB 和 1MB
        TransferManagerConfiguration transferManagerConfiguration = new TransferManagerConfiguration();
        //分块上传阈值
        transferManagerConfiguration.setMultipartUploadThreshold(CHUNK_FILE_SIZE_THRESHOLD);
        //分块大小
        transferManagerConfiguration.setMinimumUploadPartSize(CHUNK_FILE_SIZE);
        transferManager.setConfiguration(transferManagerConfiguration);
        return transferManager;
    }

    /**
     * 关闭 TransferManager
     * 在确定不再通过 TransferManager 的实例调用高级接口之后，一定要关闭这个实例，防止资源泄露。
     *
     * @param transferManager TransferManager
     */
    public static void shutdownTransferManager(TransferManager transferManager) {
        // 指定参数为 true, 则同时会关闭 transferManager 内部的 COSClient 实例。
        // 指定参数为 false, 则不会关闭 transferManager 内部的 COSClient 实例。
        transferManager.shutdownNow(true);
    }

    /**
     * 显示上传进度
     *
     * @param transfer Transfer
     */
    public static void showTransferProgress(Transfer transfer) {
        // 这里的 Transfer 是异步上传结果 Upload 的父类
        log.info("上传路径: {}", transfer.getDescription());

        // transfer.isDone() 查询上传是否已经完成
        while (!transfer.isDone()) {
            try {
                // 每 2 秒获取一次进度
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                return;
            }
            TransferProgress progress = transfer.getProgress();
            long sofar = progress.getBytesTransferred();
            long total = progress.getTotalBytesToTransfer();
            double pct = progress.getPercentTransferred();
            System.out.printf("上传进度: [%d / %d] = %.02f%%\n", sofar, total, pct);
        }

        // 完成了 Completed，或者失败了 Failed
        log.info("上传结果: {}", transfer.getState());
    }

    /**
     * 分块上传对象
     * 高级接口，多线程上传
     *
     * @param bucketName 桶名
     * @param key        key
     * @param localFile  待上传的文件
     */
    public static void uploadObjectByMutiplePart(String bucketName, String key, File localFile) {
        setCosClient();
        // 使用高级接口必须先保证本进程存在一个 TransferManager 实例，如果没有则创建
        TransferManager transferManager = createTransferManager();
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, localFile);
        // 设置存储类型（如有需要，不需要请忽略此行代码）, 默认是标准(Standard), 低频(standard_ia)
        // 更多存储类型请参见 https://cloud.tencent.com/document/product/436/33417
        putObjectRequest.setStorageClass(StorageClass.Standard);

        //若需要设置对象的自定义 Headers 可参照下列代码,若不需要可省略下面这几行,
        // 对象自定义 Headers 的详细信息可参考 https://cloud.tencent.com/document/product/436/13361
        //ObjectMetadata objectMetadata = new ObjectMetadata();
        //若设置 Content-Type、Cache-Control、Content-Disposition、Content-Encoding、Expires 这五个字自定义 Headers，
        // 推荐采用 objectMetadata.setHeader()
        // 自定义header尽量避免特殊字符，使用中文前请先手动对其进行URLEncode
        //objectMetadata.setHeader("key", "value");
        //若要设置 “x-cos-meta-[自定义后缀]” 这样的自定义 Header，推荐采用
        //Map<String, String> userMeta = new HashMap<>();
        //userMeta.put("x-cos-meta-[自定义后缀]", "value");
        //objectMetadata.setUserMetadata(userMeta);
        //putObjectRequest.withMetadata(objectMetadata);

        try {
            // 高级接口会返回一个异步结果Upload
            // 可同步地调用 waitForUploadResult 方法等待上传完成，成功返回 UploadResult, 失败抛出异常
            Upload upload = transferManager.upload(putObjectRequest);
            // 打印上传进度，直到上传结束
            showTransferProgress(upload);
            UploadResult uploadResult = upload.waitForUploadResult();
            log.info("uploadResult: {}", JSON.toJSONString(uploadResult, JSONWriter.Feature.WriteMapNullValue));
        } catch (CosClientException | InterruptedException e) {
            log.error("Error occurred: ", e);
        } finally {
            // 确定本进程不再使用 transferManager 实例之后，关闭即可
            shutdownTransferManager(transferManager);
        }

    }

    /**
     * 上传本地目录
     *
     * @param bucketName                桶名
     * @param virtualDirectoryKeyPrefix cos 上 object 的前缀
     * @param directory                 要上传的文件夹的绝对路径
     * @param includeSubdirectories     是否递归上传子目录
     */
    public static void uploadDirectory(String bucketName, String virtualDirectoryKeyPrefix,
                                       File directory, boolean includeSubdirectories) {
        setCosClient();
        TransferManager transferManager = createTransferManager();
        try {
            // 返回一个异步结果Upload, 可同步的调用 waitForUploadResult 等待 upload结束, 成功返回 UploadResult, 失败抛出异常.
            MultipleFileUpload upload = transferManager.uploadDirectory(bucketName, virtualDirectoryKeyPrefix,
                    directory, includeSubdirectories);
            // 可以选择查看上传进度
            showTransferProgress(upload);
            // 或者阻塞等待完成
            upload.waitForCompletion();
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            // 确定本进程不再使用 transferManager 实例之后，关闭即可
            shutdownTransferManager(transferManager);
        }
    }

    /**
     * 同地域复制
     * 复制的源对象和目的对象属于同一个 Region
     *
     * @param srcBucketRegionStr 源 Bucket Region 。默认值：与当前 clientconfig 的 region 一致，表示统一地域复制
     * @param srcBucketName      源存储桶名称，存储桶的命名格式为 BucketName-APPID，此处填写的存储桶名称必须为此格式
     * @param srcKey             源对象键，对象键（Key）是对象在存储桶中的唯一标识。
     * @param destBucketName     目标存储桶名称，存储桶的命名格式为 BucketName-APPID，此处填写的存储桶名称必须为此格式
     * @param destKey            目的对象键，对象键（Key）是对象在存储桶中的唯一标识。
     */
    public static void copy(String srcBucketRegionStr, String srcBucketName,
                            String srcKey, String destBucketName, String destKey) {

        // 使用高级接口必须先保证本进程存在一个 TransferManager 实例，如果没有则创建
        TransferManager transferManager = createTransferManager();
        // 复制的源桶所在的地域
        Region srcBucketRegion = new Region(srcBucketRegionStr);
        CopyObjectRequest copyObjectRequest = new CopyObjectRequest(srcBucketRegion, srcBucketName,
                srcKey, destBucketName, destKey);
        try {
            Copy copy = transferManager.copy(copyObjectRequest);
            // 高级接口会返回一个异步结果 Copy
            // 可同步的调用 waitForCopyResult 等待复制结束, 成功返回 CopyResult, 失败抛出异常
            CopyResult copyResult = copy.waitForCopyResult();
            log.info("CopyResult: {}", JSON.toJSONString(copyResult, JSONWriter.Feature.WriteMapNullValue));
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            // 确定本进程不再使用 transferManager 实例之后，关闭即可
            shutdownTransferManager(transferManager);
        }
    }

    /**
     * 文件分块上传，使用普通分块接口
     * 使用RandomAccessFile，不需要一次读取整个文件
     *
     * @param bucketName 桶名
     * @param key        key
     * @param localFile  待上传的文件
     */
    public static void uploadObjectMultipartUploadWithCosClient(String bucketName, String key, File localFile) {
        RandomAccessFile file;
        setCosClient();
        InputStream inputStream = null;
        try {
            log.info("--------------------------分片上传对象开始: {} ---------------------------------", key);
            // 首先创建多部分上传并获取上传id
            log.info("在存储桶分片上传对象: {}", bucketName);
            log.info("分片上传对象 Key: {}", key);
            // Initiate the multipart upload.
            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key);
            // 分块上传的过程中，仅能通过初始化分块指定文件上传之后的 metadata
            // 需要的头部可以在这里指定
            ObjectMetadata objectMetadata = new ObjectMetadata();
            request.setObjectMetadata(objectMetadata);
            InitiateMultipartUploadResult initResult = cosClient.initiateMultipartUpload(request);
            // 获取 uploadid
            String uploadId = initResult.getUploadId();
            log.info("uploadId: {}", uploadId);

            // 每个分块上传之后都会得到一个返回值 etag，保存起来用于最后合并分块时使用
            List<PartETag> partEtags = new LinkedList<>();
            long available = localFile.length();
            log.info("FileLength: {}", available);
            if (available <= 0L) {
                throw new RuntimeException("待读取文件为空文件，读取文件失败！");
            }
            //未达到分块文件阈值大小，走普通上传
            if (available <= CHUNK_FILE_SIZE_THRESHOLD) {
                log.info("当前文件大小：{} 未达到分块文件阈值大小：{}，走普通上传！", available, CHUNK_FILE_SIZE_THRESHOLD);
                uploadObject(bucketName, key, localFile);
                return;
            }

            int partNumber = 1;
            ByteBuffer bb = ByteBuffer.allocate(CHUNK_FILE_SIZE);
            file = new RandomAccessFile(localFile, "r");
            long fileSize = file.length();
            int position = 0;
            while (position < fileSize) {
                log.info("正在上传第：{} 个文件分片....", partNumber);
                file.seek(position);
                int read = file.getChannel().read(bb);
                // Swap position and limit before reading from the buffer.
                bb.flip();
                inputStream = new ByteArrayInputStream(bb.array());
                UploadPartRequest uploadPartRequest = new UploadPartRequest();
                uploadPartRequest.setBucketName(bucketName);
                uploadPartRequest.setKey(key);
                uploadPartRequest.setUploadId(uploadId);
                uploadPartRequest.setInputStream(inputStream);
                // 设置分块的长度
                uploadPartRequest.setPartSize(read);
                // 设置要上传的分块编号，从 1 开始
                uploadPartRequest.setPartNumber(partNumber);

                UploadPartResult uploadPartResult = cosClient.uploadPart(uploadPartRequest);
                PartETag partEtag = uploadPartResult.getPartETag();
                partEtags.add(partEtag);

                bb.clear();
                position += read;
                partNumber++;
            }
            // 分片上传结束后，调用 complete 完成分片上传
            CompleteMultipartUploadRequest completeMultipartUploadRequest =
                    new CompleteMultipartUploadRequest(bucketName, key, uploadId, partEtags);
            CompleteMultipartUploadResult completeResult =
                    cosClient.completeMultipartUpload(completeMultipartUploadRequest);
            log.info("RequestId: {}", completeResult.getRequestId());

        } catch (Exception e) {
            log.error("Error occurred: ", e);
        }  finally {
            try {
                if (null != inputStream) {
                    inputStream.close();
                }
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }
    }

    /**
     * 文件分块上传，使用普通分块接口
     *
     * @param bucketName 桶名
     * @param key        key
     * @param localFile  待上传的文件
     */
    public static void uploadObjectMultipartUpload(String bucketName, String key, File localFile) {
        setCosClient();
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key);
        // 分块上传的过程中，仅能通过初始化分块指定文件上传之后的 metadata
        // 需要的头部可以在这里指定
        ObjectMetadata objectMetadata = new ObjectMetadata();
        request.setObjectMetadata(objectMetadata);
        FileInputStream fis = null;
        InputStream inputStream = null;
        try {
            InitiateMultipartUploadResult initResult = cosClient.initiateMultipartUpload(request);
            // 获取 uploadid
            String uploadId = initResult.getUploadId();
            log.info("uploadId: {}", uploadId);

            // 每个分块上传之后都会得到一个返回值 etag，保存起来用于最后合并分块时使用
            List<PartETag> partETags = new LinkedList<>();
            //文件分块数量
            int fileNum = (int) Math.ceil(localFile.length() * 1.0 / CHUNK_FILE_SIZE);
            fis = new FileInputStream(localFile);
            long available = localFile.length();
            log.info("FileLength: {}", available);
            if (available <= 0L) {
                throw new RuntimeException("待读取文件为空文件，读取文件失败！");
            }
            //未达到分块文件阈值大小，走普通上传
            if (available <= CHUNK_FILE_SIZE_THRESHOLD) {
                log.info("当前文件大小：{} 未达到分块文件阈值大小：{}，走普通上传！", available, CHUNK_FILE_SIZE_THRESHOLD);
                uploadObject(bucketName, key, localFile);
                return;
            }

            byte[] result = new byte[(int) available];
            //读取总的字节数
            int readBytesNum = fis.read(result);
            // 上传数据, 这里上传 10 个 1M 的分块数据
            for (int i = 1; i <= fileNum; i++) {
                log.info("正在上传第：{} 个文件分片....", i);
                byte[] upload;
                if (i < fileNum) {
                    upload = new byte[CHUNK_FILE_SIZE];
                    //src 源数组，srcPos 从源数组哪个位置开始拷贝，dest 目标数组，destPos 从目标数组哪个位置开始复制，length 复制多少长度
                    int startIndex = (i - 1) * CHUNK_FILE_SIZE;
                    log.debug("startIndex: {}", startIndex);
                    log.debug("endIndex: {}", startIndex + CHUNK_FILE_SIZE);
                    System.arraycopy(result, startIndex, upload, 0, CHUNK_FILE_SIZE);
                }
                //最后一块
                else {
                    upload = new byte[(int) (available - CHUNK_FILE_SIZE * (i - 1))];
                    int startIndex = (i - 1) * CHUNK_FILE_SIZE;
                    int length = (int) (available - CHUNK_FILE_SIZE * (i - 1));
                    log.debug("startIndex: {}", startIndex);
                    log.debug("endIndex: {}", startIndex + length);
                    System.arraycopy(result, startIndex, upload, 0, length);
                }
                inputStream = new ByteArrayInputStream(upload);
                UploadPartRequest uploadPartRequest = new UploadPartRequest();
                uploadPartRequest.setBucketName(bucketName);
                uploadPartRequest.setKey(key);
                uploadPartRequest.setUploadId(uploadId);
                uploadPartRequest.setInputStream(inputStream);
                // 设置分块的长度
                uploadPartRequest.setPartSize(upload.length);
                // 设置要上传的分块编号，从 1 开始
                uploadPartRequest.setPartNumber(i);

                UploadPartResult uploadPartResult = cosClient.uploadPart(uploadPartRequest);
                PartETag partETag = uploadPartResult.getPartETag();
                partETags.add(partETag);
            }
            // 分片上传结束后，调用 complete 完成分片上传
            CompleteMultipartUploadRequest completeMultipartUploadRequest =
                    new CompleteMultipartUploadRequest(bucketName, key, uploadId, partETags);
            CompleteMultipartUploadResult completeResult =
                    cosClient.completeMultipartUpload(completeMultipartUploadRequest);
            log.info("RequestId: {}", completeResult.getRequestId());

        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            try {
                if (null != fis) {
                    fis.close();
                }
                if (null != inputStream) {
                    inputStream.close();
                }
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }

    }

    /**
     * 查询分块上传任务
     *
     * @param bucketName 桶名
     * @param targetKey  分块上传任务对应的 key
     * @return uploadId
     */
    public static String listMultipartUploads(String bucketName, String targetKey) {
        setCosClient();
        ListMultipartUploadsRequest listMultipartUploadsRequest = new ListMultipartUploadsRequest(bucketName);
        // 每次请求最多列出多少个
        listMultipartUploadsRequest.setMaxUploads(100);
        // 设置要查询的分块上传任务的目标前缀，直接设置要查询的 key
        listMultipartUploadsRequest.setPrefix(targetKey);
        MultipartUploadListing multipartUploadListing;
        boolean found = false;
        String uploadId = null;
        do {
            multipartUploadListing = cosClient.listMultipartUploads(listMultipartUploadsRequest);
            List<MultipartUpload> multipartUploads = multipartUploadListing.getMultipartUploads();
            for (MultipartUpload mUpload : multipartUploads) {
                if (mUpload.getKey().equals(targetKey)) {
                    log.info("getUploadId: {}", mUpload.getUploadId());
                    uploadId = mUpload.getUploadId();
                    found = true;
                    break;
                }
            }
            if (found) {
                break;
            }
            listMultipartUploadsRequest.setKeyMarker(multipartUploadListing.getNextKeyMarker());
            listMultipartUploadsRequest.setUploadIdMarker(multipartUploadListing.getNextUploadIdMarker());
        } while (multipartUploadListing.isTruncated());
        if (!found) {
            log.info("找不到 key: {} 对应的分块上传任务", targetKey);
        }
        return uploadId;
    }

    /**
     * 查询已上传的分块
     * 查询特定分块上传操作中的已上传的块（List Parts）
     *
     * @param bucketName 桶名
     * @param key        分块上传任务对应的 key
     * @param uploadId   uploadId 是一次分块上传任务的唯一标识，通过初始化分块上传或者查询分块上传任务获得
     * @return 已上传的块（List Parts）
     */
    public static List<JSONObject> listParts(String bucketName, String key, String uploadId) {
        setCosClient();
        // 用于保存已上传的分片信息
        List<JSONObject> partEtags = new LinkedList<>();
        PartListing partListing;
        ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, key, uploadId);
        do {
            partListing = cosClient.listParts(listPartsRequest);
            for (PartSummary partSummary : partListing.getParts()) {
                PartETag partEtag = new PartETag(partSummary.getPartNumber(), partSummary.getETag());
                JSONObject part = new JSONObject();
                part.put("partNumber", partEtag.getPartNumber());
                part.put("eTag", partEtag.getETag());
                partEtags.add(part);
            }
            listPartsRequest.setPartNumberMarker(partListing.getNextPartNumberMarker());
        } while (partListing.isTruncated());
        return partEtags;
    }

    /**
     * 终止分块上传（终止一个分块上传操作并删除已上传的分块）
     * 终止分块上传之后，分块上传任务和已经上传的分块都会被删除，对应的 uploadId 不再有效。
     *
     * @param bucketName 桶名
     * @param key        分块上传任务对应的 key
     * @param uploadId   uploadId 是一次分块上传任务的唯一标识
     */
    public static void abortMultipartUpload(String bucketName, String key, String uploadId) {
        AbortMultipartUploadRequest abortMultipartUploadRequest =
                new AbortMultipartUploadRequest(bucketName, key, uploadId);
        try {
            cosClient.abortMultipartUpload(abortMultipartUploadRequest);
        } catch (CosClientException e) {
            log.error("Error occurred: ", e);
        }

    }
}
